/*
 * Copyright 2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.seppiko.chart.utils

import com.d_project.qrcode.QRCode
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.encoder.ByteMatrix
import org.apache.batik.dom.GenericDOMImplementation
import org.apache.batik.svggen.SVGGeneratorContext
import org.apache.batik.svggen.SVGGraphics2D
import org.apache.batik.svggen.SVGGraphics2DIOException
import org.seppiko.chart.exceptions.SeppikoCheckException
import org.seppiko.chart.exceptions.SeppikoImageGeneratorException
import org.seppiko.chart.exceptions.SeppikoProcessorException
import org.seppiko.chart.models.BarcodeEntity
import org.seppiko.chart.utils.DProjectUtil.createGIFImage
import java.awt.*
import java.awt.image.BufferedImage
import java.io.*
import java.nio.charset.StandardCharsets
import javax.imageio.ImageIO


/**
 * @author Leonard Woo
 */
object ZxingImageUtil {
  private val logger = LoggingManager.INSTANCE.getLogger(ZxingImageUtil::class.qualifiedName!!)

  const val BLACK = 0x000000
  const val WHITE = 0xFFFFFF

  @Throws(SeppikoCheckException::class, SeppikoProcessorException::class)
  fun imageGenerator(entity: BarcodeEntity, matrix: ByteMatrix): ByteArray? {
    val image = toBufferedImage(matrix, entity.height, entity.width, entity.margin,
      Integer.decode(entity.color), Integer.decode(entity.backgroundColor))
    return writeImageBytes(entity, image)
  }

  @Throws(SeppikoCheckException::class, SeppikoProcessorException::class)
  fun imageGenerator(entity: BarcodeEntity, matrix: BitMatrix): ByteArray? {
    val image = toBufferedImage(matrix,
      Integer.decode(entity.color), Integer.decode(entity.backgroundColor))
    return writeImageBytes(entity, image)
  }

  @Throws(SeppikoCheckException::class, SeppikoProcessorException::class)
  fun imageGenerator(entity: BarcodeEntity, matrix: BooleanArray): ByteArray? {
    val image = toBufferedImage(matrix, entity.height, entity.width, entity.margin,
      Integer.decode(entity.color), Integer.decode(entity.backgroundColor))
    return writeImageBytes(entity, image)
  }

  @Throws(SeppikoCheckException::class, SeppikoImageGeneratorException::class)
  fun imageGenerator(entity: BarcodeEntity, qrcode: QRCode): ByteArray? {
    try {
      ByteArrayOutputStream().use { out ->
        return if ("gif" == entity.format.lowercase()) {
          createGIFImage(qrcode, entity.size, entity.margin).write(out)
          out.toByteArray()
        } else {
          val image: BufferedImage = qrcode.createImage(entity.size, entity.margin)
          writeImageBytes(entity, image)
        }
      }
    } catch (e: IOException) {
      logger.warn("Image generator failed.", e)
      throw SeppikoImageGeneratorException()
    }
  }

  @Throws(SeppikoImageGeneratorException::class)
  fun svgGenerator(entity: BarcodeEntity, matrix: ByteMatrix?): ByteArray? {
    try {
      ByteArrayOutputStream().use { out ->
        val content: String? = if (entity.needText) entity.data else null
        toSVGDocument(matrix!!, content,
          OutputStreamWriter(BufferedOutputStream(out), StandardCharsets.ISO_8859_1.name()),
          entity.width, entity.height, entity.margin,
          Integer.decode(entity.color),
          Integer.decode(entity.backgroundColor)
        )
        return out.toByteArray()
      }
    } catch (ex: IOException) {
      throw SeppikoImageGeneratorException(ex.message, ex)
    }
  }

  @Throws(SeppikoImageGeneratorException::class)
  fun svgGenerator(entity: BarcodeEntity, matrix: BooleanArray?): ByteArray? {
    try {
      ByteArrayOutputStream().use { out ->
        val content: String? = if (entity.needText) entity.data else null
        toSVGDocument(
          matrix!!,
          OutputStreamWriter(
            BufferedOutputStream(out),
            StandardCharsets.ISO_8859_1.name()
          ),
          content,
          entity.width,
          entity.height,
          entity.margin,
          Integer.decode(entity.color),
          Integer.decode(entity.backgroundColor)
        )
        return out.toByteArray()
      }
    } catch (ex: IOException) {
      throw SeppikoImageGeneratorException(ex.message, ex)
    }
  }

  @Throws(SeppikoCheckException::class, SeppikoProcessorException::class)
  private fun writeImageBytes(entity: BarcodeEntity, image: BufferedImage?): ByteArray? {
    try {
      val out = ByteArrayOutputStream()
      var img: BufferedImage? = image ?: throw SeppikoCheckException("Image is null")

      if (entity.needText) {
        img = img?.let { ImageUtil.addTextOnBottom(it, entity.data, 10) }
      }

      if (!ImageIO.write(img, entity.format.lowercase(), out)) {
        val errorMsg = "Could not write an image of format " + entity.format.lowercase()
        logger.error(errorMsg)
        throw SeppikoImageGeneratorException(errorMsg)
      }

      return out.toByteArray()
    } catch (e: IOException) {
      logger.warn("Image generator failed.", e)
      throw SeppikoProcessorException()
    }
  }

  /*
   * Note:
   * That the input matrix uses 0 == white, 1 == black, while the output
   * matrix uses 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap).
   */
  private fun toBufferedImage(input: ByteMatrix, height: Int, width: Int, margin: Int, color: Int,
                      backgroundColor: Int): BufferedImage {
    return toBufferedImage(toBitMatrix(input, 1.toByte(), height, width, margin), backgroundColor, color)
  }

  private fun toBufferedImage(code: BooleanArray, width: Int, height: Int, margin: Int, backgroundColor: Int,
                      color: Int): BufferedImage {
    return toBufferedImage(renderResult(code, width, height, margin), backgroundColor, color)
  }

  private fun toBufferedImage(matrix: BitMatrix, backgroundColor: Int, color: Int): BufferedImage {
    val width = matrix.width
    val height = matrix.height
    val image = BufferedImage(
      width, height,
      if (color == BLACK && backgroundColor == WHITE) BufferedImage.TYPE_BYTE_GRAY else BufferedImage.TYPE_INT_RGB
    )
    for (x in 0 until width) {
      for (y in 0 until height) {
        image.setRGB(x, y, if (matrix[x, y]) color else backgroundColor)
      }
    }
    return image
  }

  private fun initSVGGraphics2D(): SVGGraphics2D {
    val domImpl = GenericDOMImplementation.getDOMImplementation()
    // Create an instance of org.w3c.dom.Document.
    val document = domImpl.createDocument("http://www.w3.org/2000/svg", "svg", null)
    // Create an instance of the SVG Generator.
    val ctx = SVGGeneratorContext.createDefault(document)
    ctx.comment = "Generated by Batik SVG Generator"
    return SVGGraphics2D(ctx, false)
  }

  @Throws(SeppikoImageGeneratorException::class)
  private fun toSVGDocument(matrix: ByteMatrix, content: String?, out: Writer?, width: Int, height: Int,
                    margin: Int, color: Int, backgroundColor: Int) {
    val fg = Color(color)
    val bg = Color(backgroundColor)
    val inputWidth = matrix.width
    val inputHeight = matrix.height
    val matrixWidth = inputWidth + margin * 2
    val matrixHeight = inputHeight + margin * 2
    val outputWidth = width.coerceAtLeast(matrixWidth)
    val outputHeight = height.coerceAtLeast(matrixHeight)
    val multiple = (outputWidth / matrixWidth).coerceAtMost(outputHeight / matrixHeight)
    //    int cellWidth = outputWidth / matrixWidth;
//    int cellHeight = outputHeight / matrixHeight;
    val leftPadding = (outputWidth - inputWidth * multiple) / 2
    val topPadding = (outputHeight - inputHeight * multiple) / 2

//    int fontsize = 12 * (width / 128);
//    if(fontsize <= 0 ) {
//      fontsize = 12;
//    }
    val font = Font("Arial", Font.PLAIN, 18)
    val fontSize = if (content != null) font.size else 0
    //    int cellHeight = outputHeight - margin * 2 - fontSize;
    val g2d = initSVGGraphics2D()

//    Element root = g2d.getRoot();
//    root.setAttributeNS(null, "viewBox", "0 0 " + outputWidth + " " + (outputHeight + fontSize) );
    g2d.svgCanvasSize = Dimension(outputWidth, outputHeight + fontSize)
    //    root.setAttributeNS(null, "width", "" + outputWidth);
//    root.setAttributeNS(null, "height", "" + (outputHeight + fontSize) );
    g2d.color = bg
    g2d.fill(Rectangle(0, 0, outputWidth, outputHeight + fontSize))
    g2d.color = fg
    //    g2d.setStroke(new BasicStroke(1.0f));
    var inputY = 0
    var outputY = topPadding
    while (inputY < inputHeight) {
      var inputX = 0
      var outputX = leftPadding
      while (inputX < inputWidth) {
        if (matrix[inputX, inputY].toInt() == 1) {
//          if (g2d.getColor() != fg) {
//            g2d.setColor(fg);
//          }
          g2d.fill(Rectangle(outputX, outputY, multiple, multiple))
          //        } else {
//          if (g2d.getColor() != bg) {
//            g2d.setColor(bg);
//          }
//          g2d.fill(new Rectangle(outputX, outputY, multiple, multiple));
        }
        inputX++
        outputX += multiple
      }
      inputY++
      outputY += multiple
    }
    if (content != null) {
      val fm = g2d.getFontMetrics(font)
      val textWidth = fm.stringWidth(content)
      val fontX = (outputWidth / 2 - textWidth / 2).toFloat()
      logger.info("X >>> $textWidth >>>$fontX")
      val fontY = margin + outputHeight + (font.size + 0.0f) / 2
      logger.info("Y >>> " + font.size + " >>>" + fontY)
      g2d.font = font
      g2d.drawString(content, fontX, fontY)
    }
    try {
//      g2d.stream(root, out, false, false);
      g2d.stream(out, false)
    } catch (ex: SVGGraphics2DIOException) {
      logger.error("SVG create is fail.", ex)
      throw SeppikoImageGeneratorException()
    } finally {
      g2d.dispose()
    }
  }

  @Throws(SeppikoImageGeneratorException::class)
  private fun toSVGDocument(code: BooleanArray, out: Writer?, content: String?, width: Int, height: Int,
                    margin: Int, color: Int, backgroundColor: Int) {
    val fg = Color(color)
    val bg = Color(backgroundColor)
    var fontsize = 12 * (width / 128)
    if (fontsize <= 0) {
      fontsize = 12
    }
    val font = Font("Arial", Font.PLAIN, fontsize)
    val inputWidth = code.size

//    int matrixWidth = inputWidth + (margin * 2);
//    int matrixHeight = height + (margin * 2);

//    int outputWidth = Math.max(width, matrixWidth);
//    int outputHeight = Math.max(height, matrixHeight);
    val cellWidth = Math.round((width - margin * 2).toFloat() / inputWidth.toFloat())

//    int cellHeight = outputHeight / matrixHeight;
    val cellHeight = height - margin * 2 - if (content != null) font.size else 0
    val outputWidth = Math.max(width, cellWidth * inputWidth + margin * 2)

//    int leftPadding = (outputWidth - (inputWidth * cellWidth)) / 2;
//    int topPadding = (outputHeight - (height * cellHeight)) / 2;
    val g2d = initSVGGraphics2D()

//    Element root = g2d.getRoot();
//    root.setAttributeNS(null, "viewBox", "0 0 " + outputWidth + " " + height );
    g2d.svgCanvasSize = Dimension(outputWidth, height)
    //    root.setAttributeNS(null, "width", "" + outputWidth);
//    root.setAttributeNS(null, "height", "" + height );
    g2d.color = bg
    g2d.fill(Rectangle(0, 0, outputWidth, height))
    g2d.color = fg
    g2d.stroke = BasicStroke()
    var inputX = 0
    var outputX = margin
    while (inputX < inputWidth) {
      if (code[inputX]) {
//        if (g2d.getColor() != fg) {
//          g2d.setColor(fg);
//        }
        g2d.fill(Rectangle(outputX, margin, cellWidth, cellHeight))
        //      } else {
//        if (g2d.getColor() != bg) {
//          g2d.setColor(bg);
//        }
//        g2d.fill(new Rectangle(outputX, codeHeight, cellWidth, cellHeight));
      }
      inputX++
      outputX += cellWidth
    }
    if (content != null) {
      val fm = g2d.getFontMetrics(font)
      val textWidth = fm.stringWidth(content)
      val fontX = (outputWidth / 2 - textWidth / 2).toFloat()
      logger.info(">>> $textWidth >>>$fontX")
      val fontY = (margin + cellHeight + font.size).toFloat()
      g2d.font = font
      g2d.drawString(content, fontX, fontY)
    }
    try {
//      g2d.stream(root, out, false, false);
      g2d.stream(out, false)
    } catch (ex: SVGGraphics2DIOException) {
      logger.error("SVG create is fail.", ex)
      throw SeppikoImageGeneratorException()
    } finally {
      g2d.dispose()
    }
  }

  // see com.google.zxing.oned.OneDimensionalCodeWriter.renderResult
  private fun renderResult(code: BooleanArray, width: Int, height: Int, margin: Int): BitMatrix {
    val inputWidth = code.size
    val fullWidth = inputWidth + margin
    val outputWidth = width.coerceAtLeast(fullWidth)
    val outputHeight = 1.coerceAtLeast(height)
    val multiple = outputWidth / fullWidth
    val leftPadding = (outputWidth - inputWidth * multiple) / 2
    val output = BitMatrix(outputWidth, outputHeight)
    var inputX = 0
    var outputX = leftPadding
    while (inputX < inputWidth) {
      if (code[inputX]) {
        output.setRegion(outputX, 0, multiple, outputHeight)
      }
      ++inputX
      outputX += multiple
    }
    return output
  }

  private fun toBitMatrix(matrix: ByteMatrix, flag: Byte, height: Int, width: Int, margin: Int): BitMatrix {
    val inputWidth = matrix.width
    val inputHeight = matrix.height
    val qrWidth = inputWidth + margin * 2
    val qrHeight = inputHeight + margin * 2
    val outputWidth = width.coerceAtLeast(qrWidth)
    val outputHeight = height.coerceAtLeast(qrHeight)
    val multiple = (outputWidth / qrWidth).coerceAtMost(outputHeight / qrHeight)
    return toBitMatrix(matrix, flag, outputHeight, outputWidth, multiple, multiple)
  }

  private fun toBitMatrix(matrix: ByteMatrix, flag: Byte, height: Int, width: Int, cellHeight: Int,
                          cellWidth: Int): BitMatrix {
    val inputWidth = matrix.width
    val inputHeight = matrix.height
    val leftPadding = (width - inputWidth * cellWidth) / 2
    val topPadding = (height - inputHeight * cellHeight) / 2
    val output = BitMatrix(width, height)
    var inputY = 0
    var outputY = topPadding
    while (inputY < inputHeight) {
      var inputX = 0
      var outputX = leftPadding
      while (inputX < inputWidth) {
        if (matrix[inputX, inputY] == flag) {
          output.setRegion(outputX, outputY, cellWidth, cellHeight)
        }
        ++inputX
        outputX += cellWidth
      }
      ++inputY
      outputY += cellHeight
    }
    return output
  }

}